<?php defined('SYS_PATH') or die('No direct script access.');
/**
 * Http class
 *
 * @author zendzhang<zendzhang@hotmail.com>
 * @version $Id$
 */
class TrueBlood_Http extends TrueBlood
{
    /**
     * An array of cURL options default to set.
     *
     * @var array
     */
    protected $_defaults = array
    (
        CURLOPT_HTTP_VERSION    => CURL_HTTP_VERSION_1_1,
        CURLOPT_CONNECTTIMEOUT  => 10,
        CURLOPT_TIMEOUT         => 30,
        CURLOPT_HEADER          => true,
        CURLOPT_RETURNTRANSFER  => true,
        CURLOPT_FOLLOWLOCATION  => true,
    );

    /**
     * An array of cURL options to set.
     *
     * @var array
     */
    protected $_options = array();

    /**
     * An array of HTTP header fields to set.
     *
     * @var array
     */
    protected $_headers = array();

    /**
     * An array of "Set-Cookie: " header field to set.
     *
     * @var array
     */
    protected $_cookies = array();

    /**
     * An array of streaming setting.
     *
     * @var array
     */
    protected $_streams = array();

    /**
     * Max retries
     *
     * @var int
     */
    protected $_maxRetries = 1;

    /**
     * Retried count
     *
     * @var int
     */
    protected $_redirects = 0;

    protected $my = "abc";

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->_defaults = $this->load->config('http') + $this->_defaults;

        $this->log('debug', "TrueBlood_Http Class Initialized");
    }

    /**
     * Sets request option
     *
     * @param mixed $option
     * @param mixed $value
     * @return void
     */
    public function setOption($option, $value = null)
    {
        if (!is_array($option))
        {
            $option = array($option => $value);
        }
        $this->_options = $option + $this->_options;
    }

    /**
     * Sets request header
     *
     * @param mixed $header
     * @param mixed $value
     * @return void
     */
    public function setHeader($header, $value = null)
    {
        if (is_string($header))
        {
            if (false !== strpos($header, ':'))
            {
                list($k, $v) = explode(':', $header, 2);
                $header = array(trim($k) => trim($v));
            }
            else
            {
                $header = array($header => $value);
            }
        }
        $this->_headers = (array)$header + $this->_headers;
    }

    /**
     * Sets cookie datas
     *
     * @param mixed $cookie
     * @param mixed $value
     * @return void
     */
    public function setCookie($cookie, $value = null)
    {
        if (is_string($cookie))
        {
            if (false !== strpos($cookie, '='))
            {
                $pairs = explode(';', $cookie);
                $cookie = array();
                foreach ($pairs as $pair)
                {
                    list($k, $v) = explode('=', $pair, 2);
                    $cookie = array(trim($k) => trim($v)) + $cookie;
                }
            }
            else
            {
                $cookie = array($cookie => $value);
            }
        }
        $this->_cookies = $cookie + $this->_cookies;
    }

    /**
     * Sets request stream
     *
     * @param array $stream Stream options[stream,length|seek]
     * @return void
     */
    public function setStream(array $stream)
    {
        if (isset($stream['stream']))
        {
            $this->_streams = $stream;
        }
        else
        {
            $this->_streams = array();
        }
    }

    /**
     * HTTP GET request
     *
     * @param string $url
     * @return object
     */
    public function get($url)
    {
        return $this->sendRequest('GET', $url);
    }

    /**
     * HTTP POST request
     *
     * @param string $url
     * @param mixed $content
     * @return object
     */
    public function post($url, $content = null)
    {
        return $this->sendRequest('POST', $url, $content);
    }

    /**
     * HTTP PUT request
     *
     * @param string $url
     * @param mixed $content
     * @return object
     */
    public function put($url, $content = null)
    {
        return $this->sendRequest('PUT', $url, $content);
    }

    /**
     * HTTP DELETE request
     *
     * @param string $url
     * @return object
     */
    public function delete($url)
    {
        return $this->sendRequest('DELETE', $url);
    }

    /**
     * HTTP HEAD request
     *
     * @param string $url
     * @return object
     */
    public function head($url)
    {
        return $this->sendRequest('HEAD', $url);
    }

    /**
     * Send HTTP request, Retrieves remote contents and parse the result into object
     *
     * @param string $method HTTP method
     * @param string $url Request URL
     * @param string $content Request content
     * @return mixed
     */
    protected function sendRequest($method, $url, $content = null)
    {
        $logs = array('method' => $method, 'url' => $url, 'redirects' => $this->_redirects);

        set_time_limit(0);

        $ch = $this->prepRequest($method, $url, $content);
        $result = curl_exec($ch);
        $this->logMessage('debug', $logs);

        if ($result === false)
        {
            $logs['error'] = curl_error($ch) . '(' . curl_errno($ch) . ')';
            $this->logMessage('error', $logs);
        }
        $response = $this->processResponse($ch, $result);
        if (false !== $response || in_array($response->status, array(400, 500, 503)))
        {
            $logs['error'] = 'HTTP response status invalid: ' . $response->status;
            $this->logMessage('error', $logs);

            if ($this->_redirects < $this->_maxRetries)
            {
                $delay = (integer) (pow(4, $this->_redirects) * 100000);
                usleep($delay);
                $this->_redirects++;
                $response = $this->sendRequest($method, $url, $content);
            }
        }
        curl_close($ch);
        $this->_redirects = 0;
        $this->_options = $this->_headers = $this->_cookies = $this->_streams = array();

        return $response;
    }

    /**
     * Prepares and adds the details of the cURL request.
     *
     * @param string $method HTTP method
     * @param string $url Request URL
     * @param string $content Request content
     * @return resource
     */
    protected function prepRequest($method, $url, $content = null)
    {
        if (!extension_loaded('curl'))
        {
            throw new TrueBlood_Exception('Can not find curl extension.');
        }
        $ch = curl_init($url);
        // options
        $requestOptions = $this->_defaults;
        if (!empty($this->_options))
        {
            $requestOptions = $this->_options + $requestOptions;
        }
        curl_setopt_array($ch, $requestOptions);
        curl_setopt($ch, CURLINFO_HEADER_OUT, true);
        curl_setopt($ch, CURLOPT_USERAGENT, sprintf('TrueBlood (%s) / PHP (%s)', VERSION, phpversion()));
        // HTTP method
        switch ($method)
        {
            case 'PUT' :
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
                if (!empty($this->_streams))
                {
                    $stream = new TrueBlood_Http_Stream($this->_streams, true);
                    curl_setopt($ch, CURLOPT_READFUNCTION, array(&$stream, 'readStreamCallback'));
                    curl_setopt($ch, CURLOPT_INFILESIZE, (int) $stream->length);
                    curl_setopt($ch, CURLOPT_UPLOAD, true);
                }
                else if (null !== $content)
                {
                    curl_setopt($ch, CURLOPT_POSTFIELDS, $content);
                }
                break;
            case 'POST' :
                curl_setopt($ch, CURLOPT_POST, true);
                curl_setopt($ch, CURLOPT_POSTFIELDS, $content);
                break;
            case 'HEAD' :
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
                curl_setopt($ch, CURLOPT_NOBODY, 1);
                break;
            default :
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
                if (!empty($this->_streams))
                {
                    $stream = new TrueBlood_Http_Stream($this->_streams, false);
                    curl_setopt($ch, CURLOPT_WRITEFUNCTION, array(&$stream, 'writeStreamCallback'));
                    curl_setopt($ch, CURLOPT_HEADER, false);
                }
                else if (null !== $content)
                {
                    curl_setopt($ch, CURLOPT_POSTFIELDS, $content);
                }
                break;
        }
        // HTTP header
        $requestHeaders = array();
        if (!empty($this->_headers))
        {
            foreach ($this->_headers as $key => $value)
            {
                $requestHeaders[] = trim($key) . ': ' . trim($value);
            }
            curl_setopt($ch, CURLOPT_HTTPHEADER, $requestHeaders);
        }
        // Cookies
        $requestCookies = '';
        if (!empty($this->_cookies))
        {
            foreach ($this->_cookies as $key => $value)
            {
                $requestCookies .= $requestCookies ? ';' : '';
                $requestCookies .= rawurlencode($key) . '=' . rawurlencode($value);
            }
            curl_setopt($ch, CURLOPT_COOKIE, $requestCookies);
        }

        return $ch;
    }

    /**
     * Process HTTP response
     *
     * @param resource $ch
     * @param string $result
     * @return mixed
     */
    protected function processResponse($ch, $response)
    {
        if (is_resource($ch) && false !== $response)
        {
            $array = explode("\r\n\r\n", $response);
            $body = trim(array_pop($array));
            $header = trim(array_pop($array));
            $code = (integer) curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $info = curl_getinfo($ch);

            $header = explode("\r\n", $header);
            array_shift($header);
            $assoc = array();
            foreach ($header as $line)
            {
                $arr = explode(': ', $line);
                $assoc[$arr[0]] = isset($arr[1]) ? $arr[1] : '';
            }

            return new TrueBlood_Http_Response($assoc, $body, $code, $info);
        }

        return false;
    }

    /**
     * Logs message
     *
     * @param string $level
     * @param array $logs
     * @return void
     */
    protected function logMessage($level, array $logs)
    {
        parent::log($level, '[CURL]' . "\t" . implode("\t", $logs));

        if ($this->_options[CURLOPT_VERBOSE])
        {
            echo "<!--\n";
            echo "---------------------------\n";
            foreach ($logs as $k => $v)
            {
                echo $k . " : " . $v . "\n";
            }
            echo "-->\n";
        }
    }
}

/**
 * Http response class
 *
 * @author zendzhang<zendzhang@hotmail.com>
 * @version $Id$
 */
class TrueBlood_Http_Response
{
    /**
     * HTTP response code
     *
     * @var int
     */
    public $status;

    /**
     * HTTP response headers
     *
     * @var array
     */
    public $header = array();

    /**
     * HTTP response body
     *
     * @var string
     */
    public $body;

    /**
     * cURL information
     *
     * @var string
     */
    public $info = array();

    /**
     * Constructor
     *
     * @param array $header
     * @param string $body
     * @param int $status
     * @param array $info
     * @return void
     */
    function __construct($header, $body, $status, $info)
    {
        $this->header = $header;
        $this->body = $body;
        $this->status = $status;
        $this->info = $info;
    }

    /**
     * Did we receive the status code we expected?
     *
     * @param int $ |array $codes
     * @return bool
     */
    public function isOK($codes = array(200, 201, 204, 206))
    {
        if (is_array($codes))
        {
            return in_array($this->status, $codes);
        }

        return $this->status === $codes;
    }
}

/**
 * Http stream class
 *
 * @author zendzhang<zendzhang@hotmail.com>
 * @version $Id$
 */
class TrueBlood_Http_Stream
{
    /**
     * File to read/while to.
     *
     * @var string
     */
    public $file;

    /**
     * The resource to read/while to.
     *
     * @var string
     */
    public $stream;

    /**
     * The size of the stream to read from.
     *
     * @var int
     */
    public $length;

    /**
     * The length already read from the stream.
     *
     * @var int
     */
    public $readin = 0;

    /**
     * Stores the intended starting seek position.
     *
     * @var int
     */
    public $seek;

    /**
     * Constructor
     *
     * @param array $streams Stream options[stream,length|seek]
     * @param bool $forRead
     */
    function __construct(array $streams, $forRead)
    {
        if ($forRead)
        {
            $this->setReadStreams($streams);
        }
        else
        {
            $this->setWriteStreams($streams);
        }
    }

    /**
     * Sets the resource to read from while streaming up.
     *
     * @param array $streams Stream options[stream,length|seek]
     * @return void
     */
    private function setReadStreams(array $streams)
    {
        if (is_resource($streams['stream']))
        {
            $this->stream = $streams['stream'];
        }
        else
        {
            $this->file = $streams['stream'];
            if (false === ($this->stream = fopen($streams['stream'], 'r')))
            {
                throw new TrueBlood_Exception('failed to open file:' . $streams['stream']);
            }
        }
        if (!isset($streams['length']) || $streams['length'] < 0)
        {
            $stats = fstat($this->stream);
            if ($stats && $stats['size'] >= 0)
            {
                $position = ftell($this->stream);
                if (false !== $position && $position >= 0)
                {
                    $streams['length'] = $stats['size'] - $position;
                }
            }
        }
        if (isset($streams['seek']) && $streams['seek'] >= 0)
        {
            $this->seek = $streams['seek'];
        }
        $this->length = $streams['length'];
    }

    /**
     * Sets the file to write to while streaming down.
     *
     * @param array $streams Stream options[stream,length|seek]
     * @return void
     */
    private function setWriteStreams(array $streams)
    {
        if (is_resource($streams['stream']))
        {
            $this->stream = $streams['stream'];
        }
        else
        {
            $this->file = $streams['stream'];
            if (false === ($this->stream = fopen($streams['stream'], 'w')))
            {
                throw new TrueBlood_Exception('failed to open file:' . $streams['stream']);
            }
        }
    }

    /**
     * A callback function that is invoked by cURL for streaming up.
     *
     * @param resource $ch The cURL handle for the request
     * @param resource $fh The open file handle resource.
     * @param int $size The maximum number of bytes to read.
     * @return string
     */
    public function readStreamCallback($ch, $fh, $size)
    {
        if ($this->readin >= $this->length)
        {
            return '';
        }

        if ($this->readin == 0 && isset($this->seek) && $this->seek !== ftell($this->stream))
        {
            if (fseek($this->stream, $this->seek) !== 0)
            {
                throw new TrueBlood_Exception('Failed to seek position for reading stream');
            }
        }

        $read = fread($this->stream, min($this->length - $this->readin, $size));
        $this->readin += strlen($read);

        $data = $read === false ? '' : $read;
        return $data;
    }

    /**
     * A callback function that is invoked by cURL for streaming down.
     *
     * @param resource $ch The cURL handle for the request.
     * @param string $data The data to write.
     * @return int
     */
    public function writeStreamCallback($ch, $data)
    {
        $size = strlen($data);
        $total = 0;
        $last = 0;

        while ($total < $size)
        {
            $last = fwrite($this->stream, substr($data, $total));
            if (false === $last)
            {
                return $total;
            }
            $total += $last;
        }
        return $total;
    }

    /**
     * Destructor, closes opened file handles.
     *
     * @return void
     */
    public function __destruct()
    {
        if (isset($this->file, $this->stream))
        {
            @fclose($this->stream);
        }
    }
}

?>